Azure DMS - MySQL Offline Migration Script/Invoke-DmsMySqlOffline.ps1 (461 lines of code) (raw):

<# .SYNOPSIS Script to run an offline MySQL migration scenario via DMS. .Description Script that allows MySQL offline migrations to be run within DMS. This script uses the DMS Rest APIs and has been tested with version 5.7 of the Azure "az" module and PowerShell version 7.1. As part of the caller $global:currentSubscriptionId should be set to the target subscription .PARAMETER Location Location name where the resource group needs to be created if not existing .PARAMETER ResourceGroupName Name of the resource group that contains the DMS resource. .PARAMETER ServiceName Name of the DMS resource. .PARAMETER VirtualNetworkName Name of the VNET instance to be used if DMS service needs to be created .PARAMETER SubnetName Name of the subnet in the VNET to be used if DMS service needs to be created .PARAMETER ProjectName Name of the project where the migration will be created. .PARAMETER TaskName Name of the migration task to be created. .PARAMETER SourceDatabaseName Name of the database on the source that will be migrated. .PARAMETER SourceServerName Name or IP address of the source MySQL server. .PARAMETER SourceUserName Name of the user on the source that has access to the database. .PARAMETER SourcePassword Password for the user on the source that has access to the database. .PARAMETER TargetDatabaseName Name of the target database on the target MySQL server. .PARAMETER TargetServerName Name or IP address of the target MySQL server. .PARAMETER TargetUserName Name of the user on the target that has access to the database. .PARAMETER TargetPassword Password for the user on the target that has access to the database. .PARAMETER IncludeTables Optional setting that specifies the tables to be included for migration from the source. Default value is NULL which implies include all tables from source. .PARAMETER ExcludeTables Optional setting that specifies the tables to be excluded for migration from the source. Default value is NULL which implies exclude no tables from source. ExcludeTables list has higher priority than IncludeTables list. .PARAMETER DesiredRangesCount Optional setting that configures the maximum number of parallel reads on tables located on the source database. .PARAMETER MaxBatchSizeKb Optional setting that configures that size of the largest batch that will be committed to the target server. .PARAMETER MinBatchRows Optional setting that configures the minimum number of rows in each batch written to the target. .PARAMETER PrepareDatabaseForBulkImportTaskCount Optional setting that configures the number of databases that will be prepared for migration in parallel. .PARAMETER PrepareTableForBulkImportTaskCount Optional setting that configures the number of tables that will be prepared for migration in parallel. .PARAMETER QueryTableDataRangeTaskCount Optional setting that configures the number of threads available to read ranges on the source. .PARAMETER WriteDataRangeBatchTaskCount Optional setting that configures the number of threads available to write batches to the target. .PARAMETER MaxBatchCacheSizeMb Optional setting that configures how much memory will be used to cache batches in memory before reads on the source are throttled. .PARAMETER ThrottleQueryTableDataRangeTaskAtAvailableMemoryMb Optional setting that configures the amount of available memory at which point reads on the source will be throttled. .PARAMETER ThrottleQueryTableDataRangeTaskAtBatchCount Optional setting that configures the number of batches cached in memory that will trigger read throttling on the source. .PARAMETER DelayProgressUpdatesInStorageInterval Optional setting that configures the delay between updates of result objects in Azure Table Storage. .EXAMPLE Connect-AzAccount Set-AzContext -Subscription DMSTest .\Invoke-DmsMySqlOffline.ps1 -SubscriptionName mySubscription ` -Location "westus2" ` -ResourceGroupName MySqlMigrationServiceGroup ` -ServiceName MySqlMigrationService ` -VirtualNetworkName myVnet ` -SubnetName mySubnet ` -ProjectName MySqlMigrations ` -TaskName db1Migration ` -SourceDatabaseName db1 ` -SourceServerName SourceMySqlServer ` -SourceUserName dms ` -SourcePassword <password> ` -TargetDatabaseName db1 ` -TargetServerName TargetMySqlServer.mysql.database.azure.com ` -TargetUserName dms@TargetMySqlServer ` -TargetPassword <password> ` -DesiredRangesCount 4 ` -MaxBatchSizeKb 4096 ` -QueryTableDataRangeTaskCount 12 ` -WriteDataRangeBatchTaskCount 12 ` -DelayProgressUpdatesInStorageInterval "00:00:10" ` -IncludeTables db1.tbl1, db1.tbl2 #> param( [Parameter(Mandatory = $true)] [string] $Location, [Parameter(Mandatory = $true)] [string] $ResourceGroupName, [Parameter(Mandatory = $true)] [string] $ServiceName, [Parameter(Mandatory = $true)] [string] $VirtualNetworkName, [Parameter(Mandatory = $true)] [string] $SubnetName, [Parameter(Mandatory = $true)] [string] $ProjectName, [Parameter(Mandatory = $true)] [string] $TaskName, [Parameter(Mandatory = $true)] [string] $SourceDatabaseName, [Parameter(Mandatory = $true)] [string] $SourceServerName, [Parameter(Mandatory = $true)] [string] $SourceUserName, [Parameter(Mandatory = $true)] [securestring] $SourcePassword, [Parameter(Mandatory = $true)] [string] $TargetDatabaseName, [Parameter(Mandatory = $true)] [string] $TargetServerName, [Parameter(Mandatory = $true)] [string] $TargetUserName, [Parameter(Mandatory = $true)] [securestring] $TargetPassword, # Optional table settings [Parameter(Mandatory = $false)] [string[]] $IncludeTables = $null, [Parameter(Mandatory = $false)] [string[]] $ExcludeTables = $null, # Partitioning settings [Parameter(Mandatory = $false)] [object] $DesiredRangesCount = $null, [Parameter(Mandatory = $false)] [object] $MaxBatchSizeKb = $null, [Parameter(Mandatory = $false)] [object] $MinBatchRows = $null, # Task count settings [Parameter(Mandatory = $false)] [object] $PrepareDatabaseForBulkImportTaskCount = $null, [Parameter(Mandatory = $false)] [object] $PrepareTableForBulkImportTaskCount = $null, [Parameter(Mandatory = $false)] [object] $QueryTableDataRangeTaskCount = $null, [Parameter(Mandatory = $false)] [object] $WriteDataRangeBatchTaskCount = $null, # Batch cache settings [Parameter(Mandatory = $false)] [object] $MaxBatchCacheSizeMb = $null, [Parameter(Mandatory = $false)] [object] $ThrottleQueryTableDataRangeTaskAtAvailableMemoryMb = $null, [Parameter(Mandatory = $false)] [object] $ThrottleQueryTableDataRangeTaskAtBatchCount = $null, # Performance settings [Parameter(Mandatory = $false)] [object] $DelayProgressUpdatesInStorageInterval = $null ) function LogMessage([string] $Message, [bool] $IsProcessing = $false) { if ($IsProcessing) { Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss"): $Message" -ForegroundColor Yellow } else { Write-Host "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss"): $Message" -ForegroundColor Green } } function InitConnection( [string] $ServerName, [string] $UserName, [securestring] $Password) { $connectionInfo = @{ "dataSource" = ""; "serverName" = ""; "port" = 3306; "userName" = ""; "password" = ""; "authentication" = "SqlAuthentication"; "encryptConnection" = $true; "trustServerCertificate" = $true; "additionalSettings" = ""; "type" = "MySqlConnectionInfo" } $connectionInfo.dataSource = $ServerName; $connectionInfo.serverName = $ServerName; $connectionInfo.userName = $UserName; $connectionInfo.password = (ConvertFrom-SecureString -AsPlainText $password).ToString(); $connectionInfo; } function RunScenario([object] $MigrationService, [object] $MigrationProject, [string] $ScenarioTaskName, [object] $TaskProperties, [bool] $WaitForScenario = $true) { # Check if the scenario task already exists, if so remove it LogMessage -Message "Removing scenario if already exists..." -IsProcessing $true Remove-AzDataMigrationTask ` -ResourceGroupName $MigrationService.ResourceGroupName ` -ServiceName $MigrationService.Name ` -ProjectName $MigrationProject.Name ` -TaskName $ScenarioTaskName ` -Force; # Start the new scenario task using the provided properties LogMessage -Message "Initializing scenario..." -IsProcessing $true New-AzResource ` -ApiVersion 2018-03-31-preview ` -Location $MigrationService.Location ` -ResourceId "/subscriptions/$($global:currentSubscriptionId)/resourceGroups/$($MigrationService.ResourceGroupName)/providers/Microsoft.DataMigration/services/$($MigrationService.Name)/projects/$($MigrationProject.Name)/tasks/$($ScenarioTaskName)" ` -Properties $TaskProperties ` -Force | Out-Null; LogMessage -Message "Waiting for $ScenarioTaskName scenario to complete..." -IsProcessing $true if ($WaitForScenario) { $progressCounter = 0; do { if ($null -ne $scenarioTask) { Start-Sleep 10; } # Get calls can time out and will return a cancellation exception in that case $scenarioTask = Get-AzDataMigrationTask ` -ResourceGroupName $MigrationService.ResourceGroupName ` -ServiceName $MigrationService.Name ` -ProjectName $MigrationProject.Name ` -TaskName $ScenarioTaskName ` -Expand ` -ErrorAction Ignore; Write-Progress -Activity "Scenario Run $ScenarioTaskName (Marquee Progress Bar)" ` -Status $scenarioTask.ProjectTask.Properties.State ` -PercentComplete $progressCounter $progressCounter += 10; if ($progressCounter -gt 100) { $progressCounter = 10 } } while (($null -eq $scenarioTask) -or ($scenarioTask.ProjectTask.Properties.State -eq "Running") -or ($scenarioTask.ProjectTask.Properties.State -eq "Queued")) } Write-Progress -Activity "Scenario Run $ScenarioTaskName" ` -Status $scenarioTask.ProjectTask.Properties.State ` -Completed # Now get it using REST APIs so we can expand the output LogMessage -Message "Getting expanded task results ..." -IsProcessing $true $psToken = (Get-AzAccessToken -ResourceUrl https://management.azure.com).Token; $token = ConvertTo-SecureString -String $psToken -AsPlainText -Force; $taskResource = Invoke-RestMethod ` -Method GET ` -Uri "https://management.azure.com$($scenarioTask.ProjectTask.Id)?api-version=2018-03-31-preview&`$expand=output" ` -ContentType "application/json" ` -Authentication Bearer ` -Token $token; $taskResource.properties; } function AddOptionalSetting($optionalAgentSettings, $settingName, $settingValue) { # If no value specified for the setting, don't bother adding it to the input if ($null -eq $settingValue) { return; } # Add a new property to the JSON object to capture the setting which will be customized $optionalAgentSettings | add-member -MemberType NoteProperty -Name $settingName -Value $settingValue } <#==================================Main Script===============================#> # Get the details of resource group $resourceGroup = Get-AzResourceGroup -Name $ResourceGroupName if (-not($resourceGroup)) { LogMessage -Message "Creating resource group $ResourceGroupName..." -IsProcessing $true $resourceGroup = New-AzResourceGroup -Name $ResourceGroupName -Location $Location LogMessage -Message "Created resource group - $($resourceGroup.ResourceId)." } else { LogMessage -Message "Resource group $ResourceGroupName exists." } <#====DMS Service====#> # Capture details from the current connection and get a reference to the DMS service $dmsServiceResourceId = "/subscriptions/$($global:currentSubscriptionId)/resourceGroups/$ResourceGroupName/providers/Microsoft.DataMigration/services/$ServiceName" $dmsService = Get-AzResource -ResourceId $dmsServiceResourceId -ErrorAction SilentlyContinue # Create Azure DMS service if not existing # Possible values for SKU currently are Standard_1vCore,Standard_2vCores,Standard_4vCores,Premium_4vCores if (-not($dmsService)) { $virtualNetwork = Get-AzVirtualNetwork -ResourceGroupName $ResourceGroupName -Name $VirtualNetworkName if (-not ($virtualNetwork)) { throw "ERROR: Virtual Network $VirtualNetworkName does not exists" } $subnet = Get-AzVirtualNetworkSubnetConfig -VirtualNetwork $virtualNetwork -Name $SubnetName if (-not ($subnet)) { throw "ERROR: Virtual Network $VirtualNetworkName does not contains Subnet $SubnetName" } LogMessage -Message "Creating Azure Data Migration Service $ServiceName..." -IsProcessing $true $dmsService = New-AzDataMigrationService ` -ResourceGroupName $ResourceGroupName ` -Name $ServiceName ` -Location $resourceGroup.Location ` -Sku Premium_4vCores ` -VirtualSubnetId $Subnet.Id $dmsService = Get-AzResource -ResourceId $dmsServiceResourceId LogMessage -Message "Created Azure Data Migration Service - $($dmsService.ResourceId)." } else { LogMessage -Message "Azure Data Migration Service $ServiceName exists." } <#====DMS Project====#> # Capture details from the current connection and get a reference to the DMS project $dmsProjectResourceId = "/subscriptions/$($global:currentSubscriptionId)/resourceGroups/$($dmsService.ResourceGroupName)/providers/Microsoft.DataMigration/services/$($dmsService.Name)/projects/$projectName" $dmsProject = Get-AzResource -ResourceId $dmsProjectResourceId -ErrorAction SilentlyContinue # Create Azure DMS Project if not existing if (-not($dmsProject)) { LogMessage -Message "Creating Azure DMS project $projectName for MySQL migration ..." -IsProcessing $true $newProjectProperties = @{"sourcePlatform" = "MySQL"; "targetPlatform" = "AzureDbForMySQL" } $dmsProject = New-AzResource ` -ApiVersion 2018-03-31-preview ` -Location $dmsService.Location ` -ResourceId $dmsProjectResourceId ` -Properties $newProjectProperties ` -Force; LogMessage -Message "Created Azure DMS project $projectName - $($dmsProject.ResourceId)." } else { LogMessage -Message "Azure DMS project $projectName exists." } <#====Connection Info====#> # Initialize the source and target database server connections LogMessage -Message "Initializing source and target connection objects ..." -IsProcessing $true $sourceConnInfo = InitConnection ` $SourceServerName ` $SourceUserName ` $SourcePassword; $targetConnInfo = InitConnection ` $TargetServerName ` $TargetUserName ` $TargetPassword; LogMessage -Message "Source and target connection object initialization complete." <#====Table Listing====#> # Run scenario to get the tables from the target database to build the migration table mapping # Create the get table task properties by initializing the connection and database name $getTablesTaskProperties = @{ "input" = @{ "connectionInfo" = $null; "selectedDatabases" = $null; }; "taskType" = "GetUserTablesMySql"; }; LogMessage -Message "Running scenario to get the list of tables from the target database..." -IsProcessing $true $getTablesTaskProperties.input.connectionInfo = $targetConnInfo; $getTablesTaskProperties.input.selectedDatabases = @($TargetDatabaseName); # Create a name for the task $getTableTaskName = "$($TargetDatabaseName)GetUserTables" # Get the list of tables from the source $getTargetTablesTask = RunScenario -MigrationService $dmsService ` -MigrationProject $dmsProject ` -ScenarioTaskName $getTableTaskName ` -TaskProperties $getTablesTaskProperties; if (-not ($getTargetTablesTask)) { throw "ERROR: Could not get target database $TargetDatabaseName table information." } LogMessage -Message "List of tables from the target database acquired." LogMessage -Message "Running scenario to get the list of tables from the source database..." -IsProcessing $true $getTablesTaskProperties.input.connectionInfo = $sourceConnInfo; $getTablesTaskProperties.input.selectedDatabases = @($SourceDatabaseName); # Create a name for the task $getTableTaskName = "$($SourceDatabaseName)GetUserTables" # Get the list of tables from the source $getSourceTablesTask = RunScenario -MigrationService $dmsService ` -MigrationProject $dmsProject ` -ScenarioTaskName $getTableTaskName ` -TaskProperties $getTablesTaskProperties; if (-not ($getSourceTablesTask)) { throw "ERROR: Could not get source database $SourceDatabaseName table information." } LogMessage -Message "List of tables from the source database acquired." <#====Table Mapping====#> # Create the source to target table map LogMessage -Message "Creating the table map based on the user input and database table information ..." ` -IsProcessing $true $targetTables = $getTargetTablesTask.Output.DatabasesToTables."$TargetDatabaseName"; $sourceTables = $getSourceTablesTask.Output.DatabasesToTables."$SourceDatabaseName"; $tableMap = New-Object 'system.collections.generic.dictionary[string,string]'; $schemaPrefixLength = $($SourceDatabaseName + ".").Length; $tableMappingError = $false foreach ($srcTable in $sourceTables) { # Removing the database name prefix from the table name so that comparison # can be done in cases where database name given are different $tableName = $srcTable.Name.Substring($schemaPrefixLength, ` $srcTable.Name.Length - $schemaPrefixLength) # In case the table is part of exclusion list then ignore the table if ($null -ne $ExcludeTables -and $ExcludeTables -contains $srcTable.Name) { continue; } # Either the include list is null or the table is part of the include list then add it in the mapping if ($null -eq $IncludeTables -or $IncludeTables -contains $srcTable.Name) { # Check if the table exists in the target. If not then log TABLE MAPPING ERROR if (-not ($targetTables | Where-Object { $_.name -ieq "$($TargetDatabaseName).$tableName" })) { $tableMappingError = $true Write-Host "TABLE MAPPING ERROR: $($targetTables.name) does not exists in target." -ForegroundColor Red continue; } $tableMap.Add("$($SourceDatabaseName).$tableName", "$($TargetDatabaseName).$tableName"); } } # In case of any table mapping errors identified, throw an error and stop the process if ($tableMappingError) { throw "ERROR: One or more table mapping errors were identified. Please see previous messages." } # In case no tables are in the mapping then throw error if ($tableMap.Count -le 0) { throw "ERROR: Could not create table mapping." } LogMessage -Message "Migration table mapping created for $($tableMap.Count) tables." <#====Create Task====#> # Create and run the migration scenario based on the connections # and the table mapping $offlineMigTaskProperties = @{ "input" = @{ "sourceConnectionInfo" = $null; "targetConnectionInfo" = $null; "selectedDatabases" = $null; "optionalAgentSettings" = @{ "EnableCacheBatchesInMemory" = $true; "DisableIncrementalRowStatusUpdates" = $true; }; "startedOn" = $null; }; "taskType" = "Migrate.MySql.AzureDbForMySql"; }; $offlineSelectedDatabase = @{ "name" = $null; "targetDatabaseName" = $null; "tableMap" = $null; }; LogMessage -Message "Preparing migration scenario configuration ..." -IsProcessing $true # Select the database to be migrated $offlineSelectedDatabase.name = $SourceDatabaseName; $offlineSelectedDatabase.tableMap = New-Object PSObject -Property $tableMap; $offlineSelectedDatabase.targetDatabaseName = $TargetDatabaseName; # Set connection info and the database mapping $offlineMigTaskProperties.input.sourceConnectionInfo = $sourceConnInfo; $offlineMigTaskProperties.input.targetConnectionInfo = $targetConnInfo; $offlineMigTaskProperties.input.selectedDatabases = @($offlineSelectedDatabase); $offlineMigTaskProperties.input.startedOn = [System.DateTimeOffset]::UtcNow.ToString("O"); <#====Configure Task====#> # Setting optional parameters from fine tuning the data transfer rate during migration # DEFAULT values for all the configurations is $null LogMessage -Message "Adding optional migration performance tuning configuration ..." -IsProcessing $true # Set any optional settings in the input based on parameters to this cmdlet AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "DesiredRangesCount" $DesiredRangesCount; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "MaxBatchSizeKb" $MaxBatchSizeKb; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "MinBatchRows" $MinBatchRows; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "PrepareDatabaseForBulkImportTaskCount" $PrepareDatabaseForBulkImportTaskCount; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "PrepareTableForBulkImportTaskCount" $PrepareTableForBulkImportTaskCount; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "QueryTableDataRangeTaskCount" $QueryTableDataRangeTaskCount; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "WriteDataRangeBatchTaskCount" $WriteDataRangeBatchTaskCount; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "MaxBatchCacheSizeMb" $MaxBatchCacheSizeMb; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "ThrottleQueryTableDataRangeTaskAtAvailableMemoryMb" $ThrottleQueryTableDataRangeTaskAtAvailableMemoryMb; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "ThrottleQueryTableDataRangeTaskAtBatchCount" $ThrottleQueryTableDataRangeTaskAtBatchCount; AddOptionalSetting $offlineMigTaskProperties.input.optionalAgentSettings "DelayProgressUpdatesInStorageInterval" $DelayProgressUpdatesInStorageInterval; <#====Run and Monitor====#> # Running the migration scenario LogMessage -Message "Running data migration scenario ..." -IsProcessing $true $summary = @{ "SourceServer" = $SourceServerName; "SourceDatabase" = $SourceDatabaseName; "TargetServer" = $TargetServerName; "TargetDatabase" = $TargetDatabaseName; "TableCount" = $tableMap.Count; "StartedOn" = $offlineMigTaskProperties.input.startedOn; } Write-Host "Job Summary:" -ForegroundColor Yellow Write-Host $(ConvertTo-Json $summary) -ForegroundColor Yellow $migrationResult = RunScenario -MigrationService $dmsService ` -MigrationProject $dmsProject ` -ScenarioTaskName $TaskName ` -TaskProperties $offlineMigTaskProperties LogMessage -Message "Migration completed with status - $($migrationResult.state)"; <#====Error detection and extraction====#> $dbLevelResult = $migrationResult.output | Where-Object { $_.resultType -eq "DatabaseLevelOutput" } $migrationLevelResult = $migrationResult.output | Where-Object { $_.resultType -eq "MigrationLevelOutput" } if ($dbLevelResult.exceptionsAndWarnings) { Write-Host "Following database errors were captured: $($dbLevelResult.exceptionsAndWarnings)" -ForegroundColor Red } if ($migrationLevelResult.exceptionsAndWarnings) { Write-Host "Following migration errors were captured: $($migrationLevelResult.exceptionsAndWarnings)" -ForegroundColor Red } if ($migrationResult.errors.details) { Write-Host "Following task level migration errors were captured: $($migrationResult.errors.details)" -ForegroundColor Red } return $migrationResult